package auction.server;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.log4j.Logger;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import auction.domain.Auction;
import auction.domain.User;
import auction.securechannel.AESChannel;
import auction.securechannel.Base64Channel;
import auction.securechannel.Channel;
import auction.securechannel.RSAChannel;
import auction.securechannel.TCPChannel;
import auction.synchronisation.Groupbid;

public class ClientThread extends Thread {

	public static Logger log = Logger.getLogger(ClientThread.class);
	private Socket socket;
	private String threaduser;
	private String username;
	private String command;
	private  boolean stop=false;

	private int clientTCPPort;
	private  String clientHostAddress;
	private String clientChallenge;
	private String serverChallenge;
	private SecretKey secretKey;
	private IvParameterSpec ivParameter;	
	private  Channel decoratedChannel ;

	private Key key;
	private Integer priority = 0;
	private int time;

	public ClientThread(Socket socket) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException {
		this.socket = socket;
		//get IP-Adress for Stage 4
		InetAddress addr = socket.getLocalAddress();
		clientHostAddress = addr.getHostAddress();
		//log.debug("Client-Host-Address: "+clientHostAddress);

		try {
			decoratedChannel = new RSAChannel(new Base64Channel(new TCPChannel(socket)));
			decoratedChannel.setPrivateKey(Server.getPrivateKey());

		} catch (IOException e) {
			cleanUp();
			throw e;
		} catch (NoSuchAlgorithmException e) {
			cleanUp();
			throw e;
		} catch (NoSuchPaddingException e) {
			cleanUp();
			throw e;
		} 

	}

	public void run() {

		byte[] rectmp =decoratedChannel.receive();
		if(rectmp==null){
			return;
		}
		String inputString = new String(rectmp);

		log.debug("inputString: "+(inputString));

		command = inputString;
		System.out.println("New Client connected.");

		username = "";

		if(isStop())
			shutdown();
		else{

			try {

				do  {

					//login
					if(command.startsWith("!login")){						

						//assert !login message:
						final String B64 = "a-zA-Z0-9/+";
						// login request send from the client to the auction server
						String firstMessage = command;

						if (firstMessage.matches("!login [a-zA-Z0-9_\\-]+ [0-9]+ ["+B64+"]{43}="))
							log.debug("1st message: !login matches the regex. Message: "+firstMessage);
						else{
							log.debug("1st message: !login doesn't match the regex!");

							//read new command
							command = new String(this.decoratedChannel.receive());	
							continue;//continue on top of the loop with next command
						}

						String[] input = command.split(" ");
						if (input.length != 4 ) {
							throw new IOException("Invalid client identification");
						}

						command = input[0]+" "+input[1];
						clientTCPPort = Integer.parseInt(input[2]);
						clientChallenge = input[3];


						if(command.length()>7){

							username = command.substring(7, command.length());			

							if (threaduser == null)
							{
								if(this.readKeyfile(username)){
									threaduser = username; //if no user is logged in on this client, set the user for this thread

									if (Server.userLoggedIn(username)) {
										send("User "+username+" is already logged in ");
										threaduser = null;

									}
									else{

										if(Server.usernameExists(username))
											Server.setLoggedIn(username);//if user already exists, only the boolean is set true
										else
											Server.addUser(new User(username,clientTCPPort,true));//new user added to userslist


										decoratedChannel.setPublicKey(readRSAPublicKey());

										serverChallenge = this.generateServerChallenge();

										String message = "!ok "+clientChallenge+" "+serverChallenge+" "+this.generateAESsecretKey()+" "+this.generateIVParameter();


										//assert !ok message:
										// the server's response to the client
										String secondMessage = message;

										if (secondMessage.matches("!ok ["+B64+"]{43}= ["+B64+"]{43}= ["+B64+"]{43}= ["+B64+"]{22}=="))
											log.debug("2nd message: !ok matches the regex. Message: "+secondMessage);
										else{
											log.debug("2nd message: !ok doesn't match the regex!");

											//read new command
											command = new String(this.decoratedChannel.receive());	
											continue;//continue on top of the loop with next command
										}

										//send OK-messasge to the client
										send(message);
										log.debug("message 2 sent");

										//further communication via AES Channel
										decoratedChannel = new AESChannel(new Base64Channel(new TCPChannel(socket)));
										//set secretkey und ivparam
										decoratedChannel.setSecretKey(secretKey);
										decoratedChannel.setIvParameter(ivParameter);


										//receive server-challenge
										String sc = new String(decoratedChannel.receive());
										log.debug("message 3 received");

										//assert server-challenge:
										// the last message sent by the Client
										String thirdMessage = sc;
										if( thirdMessage.matches("["+B64+"]{43}="))
											log.debug("3rd message: server-challenge matches the regex. Message: "+thirdMessage);
										else{
											log.debug("3rd message: server-challenge doesn't match the regex!");
										}

										if(serverChallenge.equals(sc)){
											send("Successfully logged in as "+username+"!");
											//update ClientList for all Clients
											Server.sendCurrentClientListToAllClients();
										}
										else
											send("Login not successfull, server-challenge doesn't match");
									}	
								}
								else{
									send("Rejected, No valid Userkey found");								}
							}
							else {
								if(username.equals(threaduser))
									send("You are already logged in!");
								else
									send("There is already another user logged in on this Client!");
							}
						}
						else 
							send("Usage: !login <username>");

					} 				

					//logout
					else if (command.equals("!logout")) {						
						if (threaduser!=null){

							username = threaduser;
							Server.logoutUser(username);
							this.threaduser = null; //set null because nobody is logged in on this client anymore
							send("!logoutSuccessfully logged out as "+username+"!");	

							//further communication via RSA Channel again
							decoratedChannel = new RSAChannel(new Base64Channel(new TCPChannel(socket)));
							decoratedChannel.setPrivateKey(Server.getPrivateKey());

							key=null;
							this.priority=0;
						}
						else
							send("You have to log in first!");

					}

					//end
					else if (command.startsWith("!end") || command.startsWith("!exit")) {
						shutdown();

					}

					//list
					else if (command.equals("!list")) {
						String answer = "!list" + Server.getAuctionList();
						if(key!=null){
							answer+="#HMAC#"+ new String( Base64.encode(createHMAC(Server.getAuctionList())));
						}

						send(answer); 						
					} 

					//create
					else if (command.startsWith("!create")) {					
						if(threaduser == null)
							send("You have to log in first!");
						else{
							if(command.length()>8)
							{


								String[] s = command.substring(8).split(" ");

								/* check input */
								boolean correctArgs =  s[0].matches("[0-9]+");
								int time = 0;


								if (correctArgs) {

									time = Integer.valueOf(s[0]); //milliseconds
									time = time*1000; //seconds
									Date current = new Date();
									int id = Server.getNextAuctionId();
									String description = command.substring(8+s[0].length()+1);
									Date end = new Date(current.getTime()+time);

									SimpleDateFormat formater = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss z");						   

									Server.createAuction(new Auction(id,description,threaduser,formater.format(end)), time);

									send("An auction '"+description+"' with id "+id+" has been created and will end on "+ formater.format(end) ); 					

								}
								else 
									send("Usage: !create <duration-in-seconds> <description> - the duration has to be a number! ");										
							}
							else
								send("Usage: !create <duration-in-seconds> <description>");
						}
					}

					//bid
					else if (command.startsWith("!bid")){
						if(threaduser == null)
							send("You have to log in first!");
						else{
							if(command.length()>5)
							{
								String[] s = command.substring(5).split(" ");

								/* check input */
								boolean correctArgs = s.length == 2 && s[0].matches("[0-9]+") && s[1].matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");
								int id = 0;
								double amount = 0;

								if (correctArgs) {
									id = Integer.valueOf(s[0]);
									amount = Double.valueOf(s[1]); 


									String ret = Server.bidAuction(threaduser, id, amount);

									if (ret.equals("ended"))
										send("The auction with id "+id+" has already been ended!");
									else if (ret.equals(""))
										send("There is no current available auction with id: "+id+".");
									else
									{								
										s = ret.split(";");

										if(threaduser.equals(s[0]) && (amount == Double.valueOf(s[1])))
											send("You successfully bid with "+amount+" on '"+s[2]+"'."); 	
										else
											send("You unsuccessfully bid with "+amount+" on '"+s[2]+"'. Current highest bid is "+s[1]);
									}								 

								}
								else 
									send("Usage: !bid <auction-id> <amount> - the id has to be a number, and the amount a float with a '.' as separator! ");							
							}
							else
								send("Usage: !bid <auction-id> <amount>");
						}

					}	
					//Changed{
					else if (command.startsWith("!groupbid")){
						if(threaduser == null)
							send("You have to log in first!");
						else{
							if(command.length()>10)
							{
								String[] s = command.substring(10).split(" ");

								/* check input */
								boolean correctArgs = s.length == 2 && s[0].matches("[0-9]+") && s[1].matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");
								int id = 0;
								double amount = 0;

								//ChangedPart {
								if (correctArgs) {

									id = Integer.valueOf(s[0]);
									amount = Double.valueOf(s[1]); 
									String ret = Server.groupbidAuction(this, id, amount);

									if (ret.equals("ended"))
										send("The auction with id "+id+" has already been ended!");
									else if(ret.equals("reject")){
										synchronized(this.priority){
											if(this.priority<10){
												this.priority++;
											}
										}
										if(this.getBidPriority()==10){
											send("No more groupids possible, please try again later (max priority reached)");
										}
										else{
											send("No more groupids possible, please try again later (priority increased)");
										}

									}
									else if (ret.equals(""))
										send("There is no auction available  with id: "+id+".");
									else
									{	
										synchronized(this.priority){
											if(this.priority>0){
												this.priority=0;
											}
											else{
												if(this.priority>-10){
													this.priority--;
												}
											}
										}
										send("!groupbid placed, waiting for confirmation (your groupbid will stay active at least 20 seconds)");									
									}		
									//ChangedPart  }									



								}
								else 
									send("Usage: !groupbid <auction-id> <amount> - the id has to be a number, and the amount a float with a '.' as separator! ");							
							}
							else
								send("Usage: !groupbid <auction-id> <amount>");
						}
					}
					//Changed}
					else if (command.startsWith("!confirm")){
						if(threaduser == null)
							send("You have to log in first!");
						else{
							if(command.length()>9)
							{
								String[] s = command.substring(9).split(" ");


								/* check input */
								boolean correctArgs = s.length == 3 && s[0].matches("[0-9]+") && s[1].matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");
								int id = 0;
								double amount = 0;
								String biduser="";

								if (correctArgs) {
									id = Integer.valueOf(s[0]);
									amount = Double.valueOf(s[1]); 
									biduser=s[2];
									Groupbid gp = Server.getGroupbit(biduser, id, amount);
									if (gp !=null){
										boolean con=false; 
										if(!gp.getBidder().equals(this.username)){
											if( Server.confirm(gp)){
												send("!block");
												time=0;
												while (time<20){

													if(gp.isConfirmed()){
														con = true;
														break;
													}
													try {
														synchronized(this){
															this.wait(1000);    
														}

													} catch (InterruptedException e) {
														log.debug(e.getMessage());

													}
													time++;
												}
											}


										}
										if(con){
											String groupbitAnswer = gp.getbidAnswer();
											if(groupbitAnswer.equals("ended")){
												send("!rejected auction ran out of time");
											}
											else if (groupbitAnswer.equals("")){

											}
											else{

												s = groupbitAnswer.split(";");

												if(gp.getBidder().equals(s[0]) && (gp.getAmaount() == Double.valueOf(s[1])))
													send("!confirmed");
												else
													send("!rejected Please use a higher bid price");

											}
										}
										else{
											gp.resetFirsConfirmation();
											send("!rejected");
										}
									}
									else{
										send("!rejected No Groupbid found");
									}

								}
								else 
									send("Usage: !confirm <auction-id> <amount> <biddinguser> - the id has to be a number, and the amount a float with a '.' as separator! ");							
							}
							else
								send("Usage: !confirm <auction-id> <amount> <biddinguser>");
						}
					}

					//signedBid
					else if (command.startsWith("!signedBid")){
						log.debug("process signedBid");

						String[] s = command.substring(11).split(" ");
						int id = Integer.valueOf(s[0]);
						double amount = Double.valueOf(s[1]);
						String client1Timestamp = s[2]+":"+s[0]+":"+s[1];
						String client2Timestamp = s[3]+":"+s[0]+":"+s[1];

						
						String ret = Server.bidAuctionFromSignedBid(threaduser, id, amount, client1Timestamp, client2Timestamp);

						if(ret.equals("fail"))
							send("SignedBid for auction "+id+" could not be verified!");
						else if (ret.equals("ended"))
							send("The auction with id "+id+" has already been ended!");
						else if (ret.equals(""))
							send("There is no current available auction with id: "+id+".");
						else
						{								
							s = ret.split(";");

							if(threaduser.equals(s[0]) && (amount == Double.valueOf(s[1])))
								send("You successfully bid with "+amount+" on '"+s[2]+"'."); 	
							else
								send("You unsuccessfully bid with "+amount+" on '"+s[2]+"'. Current highest bid is "+s[1]);
						}								 



					}

					//read new command
					command = new String(this.decoratedChannel.receive());					

				}while(command != null && !isStop());

				cleanUp();
			} catch (Exception e) {
				log.debug("Client error: "+e.getMessage());
				cleanUp();
				Server.removeClient(this);
				logoutUser();

			} 
		}
	}

	public void shutdown(){
		logoutUser();
		send("!end");
	}

	private void logoutUser(){
		if (threaduser!=null){

			username = threaduser;						
			Server.logoutUser(username); //logout user if terminated

		}
	}

	public void sendClientList(String clientList){
		//TODO:
		send("!getClientList" + Server.getClientList()); 
	}

	private void cleanUp() {
		try {
			//stop waiting process
			time=1000;
			socket.close();
		} catch (IOException e) {
			/* ignore */
		}
	}

	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final ClientThread other = (ClientThread) obj;
		if (socket == null) {
			if (other.socket != null) {
				return false;
			}
		} else if (!socket.equals(other.socket)) {
			return false;
		}
		return true;
	}

	public void send(String message) {

		decoratedChannel.send(message.getBytes());

	}

	public String getUsername() {
		return this.threaduser;
	}

	//LAB 3 STAGE 1________________________________________________________________________

	//read publickey of the logged in user
	private  PublicKey readRSAPublicKey(){
		PublicKey publicKey = null;
		String pathToPublicKey = Server.getClientKeyDir()+username+".pub.pem"; 
		log.debug("Path to PublicKey: "+pathToPublicKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPublicKey));
		} catch (FileNotFoundException e) {
			System.out.println("No PublicKey file found for user "+username);

		} 
		try {
			publicKey = (PublicKey) in.readObject();
		} catch (IOException e) {
			System.out.println("Error: Reading PublicKey file for user "+username);
		}

		try {
			in.close();
		} catch (IOException e) {
			/*ignore*/
		}

		return publicKey;
	}

	private String generateServerChallenge() throws UnsupportedEncodingException{
		// generates a 32 byte secure random number 
		SecureRandom secureRandom = new SecureRandom(); 
		final byte[] number = new byte[32]; 
		secureRandom.nextBytes(number);

		// encode into Base64 format 
		byte[] base64Number = Base64.encode(number);
		return new String (base64Number, "utf-8");

	}

	private String generateAESsecretKey() throws UnsupportedEncodingException, NoSuchAlgorithmException{

		KeyGenerator generator = KeyGenerator.getInstance("AES"); 
		// KEYSIZE is in bits 
		generator.init(256); 
		secretKey = generator.generateKey(); 

		byte[] base64Number = Base64.encode(secretKey.getEncoded());
		return new String (base64Number, "utf-8");
	}

	private String generateIVParameter() throws UnsupportedEncodingException{
		// generates a 32 byte secure random number 
		SecureRandom secureRandom = new SecureRandom(); 
		final byte[] number = new byte[16]; 
		secureRandom.nextBytes(number);

		ivParameter = new IvParameterSpec(number);

		// encode into Base64 format 
		byte[] base64Number = Base64.encode(number);
		return new String (base64Number, "utf-8");

	}

	//LAB 3 STAGE 2!!_______________________________________________________________________

	private boolean readKeyfile(String username){
		byte[] keyBytes = new byte[1024];
		String pathToSecretKey = Server.getClientKeyDir()+username+".key";
		FileInputStream fis;
		try {
			fis = new FileInputStream(pathToSecretKey);
			fis.read(keyBytes);
			fis.close();
		} catch (FileNotFoundException e1) {
			System.out.println("No secretkey found for user "+username);
			key=null;
			return false;
		}
		catch (IOException e) {
			System.out.println("Error: Reading file");
			key=null;
			return false;
		}
		byte[] input = Hex.decode(keyBytes);



		// make sure to use the right ALGORITHM for what you want to do
		// (see text)
		key = new SecretKeySpec(input,"HmacSHA256"); 
		return true;
	}
	private byte[] createHMAC(String plain){
		byte[]plainarray = plain.getBytes();
		Mac hMac;
		try {
			hMac = Mac.getInstance("HmacSHA256");
			hMac.init(key);
			hMac.update(plainarray);
			return hMac.doFinal();

		} catch (NoSuchAlgorithmException e) {
			log.debug("Error: "+e.getMessage());
		} catch (InvalidKeyException e) {
			log.debug("Error: "+e.getMessage());
		}

		// MESSAGE is the message to sign in bytes

		return null;
	}

	public boolean isStop() {
		return stop;
	}

	public void setStop(boolean stop) {
		this.stop = stop;
	}

	//Change{
	public synchronized int getBidPriority(){
		return this.priority;
	}
	public synchronized void decrBidPriority(){
		if(this.priority>0){
			this.priority--;
		}
	}
	public void closeSocket(){
		decoratedChannel.closeSocket();

	}
	//Change}

	public int getClientTCPPort() {
		return clientTCPPort;
	}

	public void setClientTCPPort(int clientTCPPort) {
		this.clientTCPPort = clientTCPPort;
	}

	public String getClientHostAddress() {
		return clientHostAddress;
	}

	public void setClientHostAddress(String clientHostAddress) {
		this.clientHostAddress = clientHostAddress;
	}
}
